/*
 * Copyright 2008-2020 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2008-2020 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2008-2020 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses>.
 */
package com.unboundid.ldap.sdk.examples;



import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.Version;
import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
import com.unboundid.ldap.sdk.controls.SortKey;
import com.unboundid.util.ColumnFormatter;
import com.unboundid.util.Debug;
import com.unboundid.util.FixedRateBarrier;
import com.unboundid.util.FormattableColumn;
import com.unboundid.util.HorizontalAlignment;
import com.unboundid.util.LDAPCommandLineTool;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.OutputFormat;
import com.unboundid.util.RateAdjustor;
import com.unboundid.util.ResultCodeCounter;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.WakeableSleeper;
import com.unboundid.util.ValuePattern;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.ControlArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.FilterArgument;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.ScopeArgument;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides a tool that can be used to search an LDAP directory
 * server repeatedly using multiple threads.  It can help provide an estimate of
 * the search performance that a directory server is able to achieve.  Either or
 * both of the base DN and the search filter may be a value pattern as
 * described in the {@link ValuePattern} class.  This makes it possible to
 * search over a range of entries rather than repeatedly performing searches
 * with the same base DN and filter.
 * <BR><BR>
 * Some of the APIs demonstrated by this example include:
 * <UL>
 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
 *       package)</LI>
 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
 *       package)</LI>
 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
 *       package)</LI>
 *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
 * </UL>
 * <BR><BR>
 * All of the necessary information is provided using command line arguments.
 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
 * class, as well as the following additional arguments:
 * <UL>
 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
 *       for the searches.  This must be provided.  It may be a simple DN, or it
 *       may be a value pattern to express a range of base DNs.</LI>
 *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
 *       search.  The scope value should be one of "base", "one", "sub", or
 *       "subord".  If this isn't specified, then a scope of "sub" will be
 *       used.</LI>
 *   <LI>"-z {num}" or "--sizeLimit {num}" -- specifies the maximum number of
 *       entries that should be returned in response to each search
 *       request.</LI>
 *   <LI>"-l {num}" or "--timeLimitSeconds {num}" -- specifies the maximum
 *       length of time, in seconds, that the server should spend processing
 *       each search request.</LI>
 *   <LI>"--dereferencePolicy {value}" -- specifies the alias dereferencing
 *       policy that should be used for each search request.  Allowed values are
 *       "never", "always", "search", and "find".</LI>
 *   <LI>"--typesOnly" -- indicates that search requests should have the
 *       typesOnly flag set to true, indicating that matching entries should
 *       only include attributes with an attribute description but no
 *       values.</LI>
 *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
 *       the searches.  This must be provided.  It may be a simple filter, or it
 *       may be a value pattern to express a range of filters.</LI>
 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
 *       attribute that should be included in entries returned from the server.
 *       If this is not provided, then all user attributes will be requested.
 *       This may include special tokens that the server may interpret, like
 *       "1.1" to indicate that no attributes should be returned, "*", for all
 *       user attributes, or "+" for all operational attributes.  Multiple
 *       attributes may be requested with multiple instances of this
 *       argument.</LI>
 *   <LI>"--ldapURL {url}" -- Specifies an LDAP URL that represents the base DN,
 *       scope, filter, and set of requested attributes that should be used for
 *       the search requests.  It may be a simple LDAP URL, or it may be a value
 *       pattern to express a range of LDAP URLs.  If this argument is provided,
 *       then none of the --baseDN, --scope, --filter, or --attribute arguments
 *       may be used.</LI>
 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
 *       concurrent threads to use when performing the searches.  If this is not
 *       provided, then a default of one thread will be used.</LI>
 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
 *       time in seconds between lines out output.  If this is not provided,
 *       then a default interval duration of five seconds will be used.</LI>
 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
 *       intervals for which to run.  If this is not provided, then it will
 *       run forever.</LI>
 *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of search
 *       iterations that should be performed on a connection before that
 *       connection is closed and replaced with a newly-established (and
 *       authenticated, if appropriate) connection.</LI>
 *   <LI>"-r {searches-per-second}" or "--ratePerSecond {searches-per-second}"
 *       -- specifies the target number of searches to perform per second.  It
 *       is still necessary to specify a sufficient number of threads for
 *       achieving this rate.  If this option is not provided, then the tool
 *       will run at the maximum rate for the specified number of threads.</LI>
 *   <LI>"--variableRateData {path}" -- specifies the path to a file containing
 *       information needed to allow the tool to vary the target rate over time.
 *       If this option is not provided, then the tool will either use a fixed
 *       target rate as specified by the "--ratePerSecond" argument, or it will
 *       run at the maximum rate.</LI>
 *   <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
 *       which sample data will be written illustrating and describing the
 *       format of the file expected to be used in conjunction with the
 *       "--variableRateData" argument.</LI>
 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
 *       complete before beginning overall statistics collection.</LI>
 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
 *       timestamps included before each output line.  The format may be one of
 *       "none" (for no timestamps), "with-date" (to include both the date and
 *       the time), or "without-date" (to include only time time).</LI>
 *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
 *       authorization v2 control to request that the operation be processed
 *       using an alternate authorization identity.  In this case, the bind DN
 *       should be that of a user that has permission to use this control.  The
 *       authorization identity may be a value pattern.</LI>
 *   <LI>"-a" or "--asynchronous" -- Indicates that searches should be performed
 *       in asynchronous mode, in which the client will not wait for a response
 *       to a previous request before sending the next request.  Either the
 *       "--ratePerSecond" or "--maxOutstandingRequests" arguments must be
 *       provided to limit the number of outstanding requests.</LI>
 *   <LI>"-O {num}" or "--maxOutstandingRequests {num}" -- Specifies the maximum
 *       number of outstanding requests that will be allowed in asynchronous
 *       mode.</LI>
 *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
 *       result codes for failed operations should not be displayed.</LI>
 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
 *       display-friendly format.</LI>
 * </UL>
 */
@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
public final class SearchRate
       extends LDAPCommandLineTool
       implements Serializable
{
  /**
   * The serial version UID for this serializable class.
   */
  private static final long serialVersionUID = 3345838530404592182L;



  // Indicates whether a request has been made to stop running.
  private final AtomicBoolean stopRequested;

  // The number of searchrate threads that are currently running.
  private final AtomicInteger runningThreads;

  // The argument used to indicate whether to operate in asynchronous mode.
  private BooleanArgument asynchronousMode;

  // The argument used to indicate whether to generate output in CSV format.
  private BooleanArgument csvFormat;

  // The argument used to indicate whether to suppress information about error
  // result codes.
  private BooleanArgument suppressErrors;

  // The argument used to indicate whether to set the typesOnly flag to true in
  // search requests.
  private BooleanArgument typesOnly;

  // The argument used to indicate that a generic control should be included in
  // the request.
  private ControlArgument control;

  // The argument used to specify a variable rate file.
  private FileArgument sampleRateFile;

  // The argument used to specify a variable rate file.
  private FileArgument variableRateData;

  // Indicates that search requests should include the assertion request control
  // with the specified filter.
  private FilterArgument assertionFilter;

  // The argument used to specify the collection interval.
  private IntegerArgument collectionInterval;

  // The argument used to specify the number of search iterations on a
  // connection before it is closed and re-established.
  private IntegerArgument iterationsBeforeReconnect;

  // The argument used to specify the maximum number of outstanding asynchronous
  // requests.
  private IntegerArgument maxOutstandingRequests;

  // The argument used to specify the number of intervals.
  private IntegerArgument numIntervals;

  // The argument used to specify the number of threads.
  private IntegerArgument numThreads;

  // The argument used to specify the seed to use for the random number
  // generator.
  private IntegerArgument randomSeed;

  // The target rate of searches per second.
  private IntegerArgument ratePerSecond;

  // The argument used to indicate that the search should use the simple paged
  // results control with the specified page size.
  private IntegerArgument simplePageSize;

  // The argument used to specify the search request size limit.
  private IntegerArgument sizeLimit;

  // The argument used to specify the search request time limit, in seconds.
  private IntegerArgument timeLimitSeconds;

  // The number of warm-up intervals to perform.
  private IntegerArgument warmUpIntervals;

  // The argument used to specify the scope for the searches.
  private ScopeArgument scope;

  // The argument used to specify the attributes to return.
  private StringArgument attributes;

  // The argument used to specify the base DNs for the searches.
  private StringArgument baseDN;

  // The argument used to specify the alias dereferencing policy for the search
  // requests.
  private StringArgument dereferencePolicy;

  // The argument used to specify the filters for the searches.
  private StringArgument filter;

  // The argument used to specify the LDAP URLs for the searches.
  private StringArgument ldapURL;

  // The argument used to specify the proxied authorization identity.
  private StringArgument proxyAs;

  // The argument used to request that the server sort the results with the
  // specified order.
  private StringArgument sortOrder;

  // The argument used to specify the timestamp format.
  private StringArgument timestampFormat;

  // A wakeable sleeper that will be used to sleep between reporting intervals.
  private final WakeableSleeper sleeper;



  /**
   * Parse the provided command line arguments and make the appropriate set of
   * changes.
   *
   * @param  args  The command line arguments provided to this program.
   */
  public static void main(final String[] args)
  {
    final ResultCode resultCode = main(args, System.out, System.err);
    if (resultCode != ResultCode.SUCCESS)
    {
      System.exit(resultCode.intValue());
    }
  }



  /**
   * Parse the provided command line arguments and make the appropriate set of
   * changes.
   *
   * @param  args       The command line arguments provided to this program.
   * @param  outStream  The output stream to which standard out should be
   *                    written.  It may be {@code null} if output should be
   *                    suppressed.
   * @param  errStream  The output stream to which standard error should be
   *                    written.  It may be {@code null} if error messages
   *                    should be suppressed.
   *
   * @return  A result code indicating whether the processing was successful.
   */
  public static ResultCode main(final String[] args,
                                final OutputStream outStream,
                                final OutputStream errStream)
  {
    final SearchRate searchRate = new SearchRate(outStream, errStream);
    return searchRate.runTool(args);
  }



  /**
   * Creates a new instance of this tool.
   *
   * @param  outStream  The output stream to which standard out should be
   *                    written.  It may be {@code null} if output should be
   *                    suppressed.
   * @param  errStream  The output stream to which standard error should be
   *                    written.  It may be {@code null} if error messages
   *                    should be suppressed.
   */
  public SearchRate(final OutputStream outStream, final OutputStream errStream)
  {
    super(outStream, errStream);

    stopRequested = new AtomicBoolean(false);
    runningThreads = new AtomicInteger(0);
    sleeper = new WakeableSleeper();
  }



  /**
   * Retrieves the name for this tool.
   *
   * @return  The name for this tool.
   */
  @Override()
  public String getToolName()
  {
    return "searchrate";
  }



  /**
   * Retrieves the description for this tool.
   *
   * @return  The description for this tool.
   */
  @Override()
  public String getToolDescription()
  {
    return "Perform repeated searches against an " +
           "LDAP directory server.";
  }



  /**
   * Retrieves the version string for this tool.
   *
   * @return  The version string for this tool.
   */
  @Override()
  public String getToolVersion()
  {
    return Version.NUMERIC_VERSION_STRING;
  }



  /**
   * Indicates whether this tool should provide support for an interactive mode,
   * in which the tool offers a mode in which the arguments can be provided in
   * a text-driven menu rather than requiring them to be given on the command
   * line.  If interactive mode is supported, it may be invoked using the
   * "--interactive" argument.  Alternately, if interactive mode is supported
   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
   * interactive mode may be invoked by simply launching the tool without any
   * arguments.
   *
   * @return  {@code true} if this tool supports interactive mode, or
   *          {@code false} if not.
   */
  @Override()
  public boolean supportsInteractiveMode()
  {
    return true;
  }



  /**
   * Indicates whether this tool defaults to launching in interactive mode if
   * the tool is invoked without any command-line arguments.  This will only be
   * used if {@link #supportsInteractiveMode()} returns {@code true}.
   *
   * @return  {@code true} if this tool defaults to using interactive mode if
   *          launched without any command-line arguments, or {@code false} if
   *          not.
   */
  @Override()
  public boolean defaultsToInteractiveMode()
  {
    return true;
  }



  /**
   * Indicates whether this tool should provide arguments for redirecting output
   * to a file.  If this method returns {@code true}, then the tool will offer
   * an "--outputFile" argument that will specify the path to a file to which
   * all standard output and standard error content will be written, and it will
   * also offer a "--teeToStandardOut" argument that can only be used if the
   * "--outputFile" argument is present and will cause all output to be written
   * to both the specified output file and to standard output.
   *
   * @return  {@code true} if this tool should provide arguments for redirecting
   *          output to a file, or {@code false} if not.
   */
  @Override()
  protected boolean supportsOutputFile()
  {
    return true;
  }



  /**
   * Indicates whether this tool should default to interactively prompting for
   * the bind password if a password is required but no argument was provided
   * to indicate how to get the password.
   *
   * @return  {@code true} if this tool should default to interactively
   *          prompting for the bind password, or {@code false} if not.
   */
  @Override()
  protected boolean defaultToPromptForBindPassword()
  {
    return true;
  }



  /**
   * Indicates whether this tool supports the use of a properties file for
   * specifying default values for arguments that aren't specified on the
   * command line.
   *
   * @return  {@code true} if this tool supports the use of a properties file
   *          for specifying default values for arguments that aren't specified
   *          on the command line, or {@code false} if not.
   */
  @Override()
  public boolean supportsPropertiesFile()
  {
    return true;
  }



  /**
   * Indicates whether the LDAP-specific arguments should include alternate
   * versions of all long identifiers that consist of multiple words so that
   * they are available in both camelCase and dash-separated versions.
   *
   * @return  {@code true} if this tool should provide multiple versions of
   *          long identifiers for LDAP-specific arguments, or {@code false} if
   *          not.
   */
  @Override()
  protected boolean includeAlternateLongIdentifiers()
  {
    return true;
  }



  /**
   * Adds the arguments used by this program that aren't already provided by the
   * generic {@code LDAPCommandLineTool} framework.
   *
   * @param  parser  The argument parser to which the arguments should be added.
   *
   * @throws  ArgumentException  If a problem occurs while adding the arguments.
   */
  @Override()
  public void addNonLDAPArguments(final ArgumentParser parser)
         throws ArgumentException
  {
    String description = "The base DN to use for the searches.  It may be a " +
         "simple DN or a value pattern to specify a range of DNs (e.g., " +
         "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  See " +
         ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
         "value pattern syntax.  This argument must not be used in " +
         "conjunction with the --ldapURL argument.";
    baseDN = new StringArgument('b', "baseDN", false, 1, "{dn}", description,
         "");
    baseDN.setArgumentGroupName("Search Arguments");
    baseDN.addLongIdentifier("base-dn", true);
    parser.addArgument(baseDN);


    description = "The scope to use for the searches.  It should be 'base', " +
         "'one', 'sub', or 'subord'.  If this is not provided, then a " +
         "default scope of 'sub' will be used.  This argument must not be " +
         "used in conjunction with the --ldapURL argument.";
    scope = new ScopeArgument('s', "scope", false, "{scope}", description,
         SearchScope.SUB);
    scope.setArgumentGroupName("Search Arguments");
    parser.addArgument(scope);


    description = "The filter to use for the searches.  It may be a simple " +
         "filter or a value pattern to specify a range of filters (e.g., " +
         "\"(uid=user.[1-1000])\").  See " + ValuePattern.PUBLIC_JAVADOC_URL +
         " for complete details about the value pattern syntax.  Exactly one " +
         "of this argument and the --ldapURL arguments must be provided.";
    filter = new StringArgument('f', "filter", false, 1, "{filter}",
         description);
    filter.setArgumentGroupName("Search Arguments");
    parser.addArgument(filter);


    description = "The name of an attribute to include in entries returned " +
         "from the searches.  Multiple attributes may be requested by " +
         "providing this argument multiple times.  If no request attributes " +
         "are provided, then the entries returned will include all user " +
         "attributes.  This argument must not be used in conjunction with " +
         "the --ldapURL argument.";
    attributes = new StringArgument('A', "attribute", false, 0, "{name}",
         description);
    attributes.setArgumentGroupName("Search Arguments");
    parser.addArgument(attributes);


    description = "An LDAP URL that provides the base DN, scope, filter, and " +
         "requested attributes to use for the search requests (the address " +
         "and port components of the URL, if present, will be ignored).  It " +
         "may be a simple LDAP URL or a value pattern to specify a range of " +
         "URLs.  See " + ValuePattern.PUBLIC_JAVADOC_URL + " for complete " +
         "details about the value pattern syntax.  If this argument is " +
         "provided, then none of the --baseDN, --scope, --filter, or " +
         "--attribute arguments may be used.";
    ldapURL = new StringArgument(null, "ldapURL", false, 1, "{url}",
         description);
    ldapURL.setArgumentGroupName("Search Arguments");
    ldapURL.addLongIdentifier("ldap-url", true);
    parser.addArgument(ldapURL);


    description = "The maximum number of entries that the server should " +
         "return in response to each search request.  A value of zero " +
         "indicates that the client does not wish to impose any limit on " +
         "the number of entries that are returned (although the server may " +
         "impose its own limit).  If this is not provided, then a default " +
         "value of zero will be used.";
    sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, "{num}",
         description, 0, Integer.MAX_VALUE, 0);
    sizeLimit.setArgumentGroupName("Search Arguments");
    sizeLimit.addLongIdentifier("size-limit", true);
    parser.addArgument(sizeLimit);


    description = "The maximum length of time, in seconds, that the server " +
         "should spend processing each search request.  A value of zero " +
         "indicates that the client does not wish to impose any limit on the " +
         "server's processing time (although the server may impose its own " +
         "limit).  If this is not provided, then a default value of zero " +
         "will be used.";
    timeLimitSeconds = new IntegerArgument('l', "timeLimitSeconds", false, 1,
         "{seconds}", description, 0, Integer.MAX_VALUE, 0);
    timeLimitSeconds.setArgumentGroupName("Search Arguments");
    timeLimitSeconds.addLongIdentifier("time-limit-seconds", true);
    timeLimitSeconds.addLongIdentifier("timeLimit", true);
    timeLimitSeconds.addLongIdentifier("time-limit", true);
    parser.addArgument(timeLimitSeconds);


    final Set<String> derefAllowedValues =
         StaticUtils.setOf("never", "always", "search", "find");
    description = "The alias dereferencing policy to use for search " +
         "requests.  The value should be one of 'never', 'always', 'search', " +
         "or 'find'.  If this is not provided, then a default value of " +
         "'never' will be used.";
    dereferencePolicy = new StringArgument(null, "dereferencePolicy", false, 1,
         "{never|always|search|find}", description, derefAllowedValues,
         "never");
    dereferencePolicy.setArgumentGroupName("Search Arguments");
    dereferencePolicy.addLongIdentifier("dereference-policy", true);
    parser.addArgument(dereferencePolicy);


    description = "Indicates that server should only include the names of " +
         "the attributes contained in matching entries rather than both " +
         "names and values.";
    typesOnly = new BooleanArgument(null, "typesOnly", 1, description);
    typesOnly.setArgumentGroupName("Search Arguments");
    typesOnly.addLongIdentifier("types-only", true);
    parser.addArgument(typesOnly);


    description = "Indicates that search requests should include the " +
         "assertion request control with the specified filter.";
    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
         "{filter}", description);
    assertionFilter.setArgumentGroupName("Request Control Arguments");
    assertionFilter.addLongIdentifier("assertion-filter", true);
    parser.addArgument(assertionFilter);


    description = "Indicates that search requests should include the simple " +
         "paged results control with the specified page size.";
    simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1,
         "{size}", description, 1, Integer.MAX_VALUE);
    simplePageSize.setArgumentGroupName("Request Control Arguments");
    simplePageSize.addLongIdentifier("simple-page-size", true);
    parser.addArgument(simplePageSize);


    description = "Indicates that search requests should include the " +
         "server-side sort request control with the specified sort order.  " +
         "This should be a comma-delimited list in which each item is an " +
         "attribute name, optionally preceded by a plus or minus sign (to " +
         "indicate ascending or descending order; where ascending order is " +
         "the default), and optionally followed by a colon and the name or " +
         "OID of the desired ordering matching rule (if this is not " +
         "provided, the the attribute type's default ordering rule will be " +
         "used).";
    sortOrder = new StringArgument(null, "sortOrder", false, 1, "{sortOrder}",
         description);
    sortOrder.setArgumentGroupName("Request Control Arguments");
    sortOrder.addLongIdentifier("sort-order", true);
    parser.addArgument(sortOrder);


    description = "Indicates that the proxied authorization control (as " +
         "defined in RFC 4370) should be used to request that operations be " +
         "processed using an alternate authorization identity.  This may be " +
         "a simple authorization ID or it may be a value pattern to specify " +
         "a range of identities.  See " + ValuePattern.PUBLIC_JAVADOC_URL +
         " for complete details about the value pattern syntax.";
    proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
         description);
    proxyAs.setArgumentGroupName("Request Control Arguments");
    proxyAs.addLongIdentifier("proxy-as", true);
    parser.addArgument(proxyAs);


    description = "Indicates that search requests should include the " +
         "specified request control.  This may be provided multiple times to " +
         "include multiple request controls.";
    control = new ControlArgument('J', "control", false, 0, null, description);
    control.setArgumentGroupName("Request Control Arguments");
    parser.addArgument(control);


    description = "The number of threads to use to perform the searches.  If " +
         "this is not provided, then a default of one thread will be used.";
    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
         description, 1, Integer.MAX_VALUE, 1);
    numThreads.setArgumentGroupName("Rate Management Arguments");
    numThreads.addLongIdentifier("num-threads", true);
    parser.addArgument(numThreads);


    description = "The length of time in seconds between output lines.  If " +
         "this is not provided, then a default interval of five seconds will " +
         "be used.";
    collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
         "{num}", description, 1, Integer.MAX_VALUE, 5);
    collectionInterval.setArgumentGroupName("Rate Management Arguments");
    collectionInterval.addLongIdentifier("interval-duration", true);
    parser.addArgument(collectionInterval);


    description = "The maximum number of intervals for which to run.  If " +
         "this is not provided, then the tool will run until it is " +
         "interrupted.";
    numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
         description, 1, Integer.MAX_VALUE, Integer.MAX_VALUE);
    numIntervals.setArgumentGroupName("Rate Management Arguments");
    numIntervals.addLongIdentifier("num-intervals", true);
    parser.addArgument(numIntervals);

    description = "The number of search iterations that should be processed " +
         "on a connection before that connection is closed and replaced with " +
         "a newly-established (and authenticated, if appropriate) " +
         "connection.  If this is not provided, then connections will not " +
         "be periodically closed and re-established.";
    iterationsBeforeReconnect = new IntegerArgument(null,
         "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
    iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments");
    iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect",
         true);
    parser.addArgument(iterationsBeforeReconnect);

    description = "The target number of searches to perform per second.  It " +
         "is still necessary to specify a sufficient number of threads for " +
         "achieving this rate.  If neither this option nor " +
         "--variableRateData is provided, then the tool will run at the " +
         "maximum rate for the specified number of threads.";
    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
         "{searches-per-second}", description, 1, Integer.MAX_VALUE);
    ratePerSecond.setArgumentGroupName("Rate Management Arguments");
    ratePerSecond.addLongIdentifier("rate-per-second", true);
    parser.addArgument(ratePerSecond);

    final String variableRateDataArgName = "variableRateData";
    final String generateSampleRateFileArgName = "generateSampleRateFile";
    description = RateAdjustor.getVariableRateDataArgumentDescription(
         generateSampleRateFileArgName);
    variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
         "{path}", description, true, true, true, false);
    variableRateData.setArgumentGroupName("Rate Management Arguments");
    variableRateData.addLongIdentifier("variable-rate-data", true);
    parser.addArgument(variableRateData);

    description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
         variableRateDataArgName);
    sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
         false, 1, "{path}", description, false, true, true, false);
    sampleRateFile.setArgumentGroupName("Rate Management Arguments");
    sampleRateFile.addLongIdentifier("generate-sample-rate-file", true);
    sampleRateFile.setUsageArgument(true);
    parser.addArgument(sampleRateFile);
    parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);

    description = "The number of intervals to complete before beginning " +
         "overall statistics collection.  Specifying a nonzero number of " +
         "warm-up intervals gives the client and server a chance to warm up " +
         "without skewing performance results.";
    warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
         "{num}", description, 0, Integer.MAX_VALUE, 0);
    warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
    warmUpIntervals.addLongIdentifier("warm-up-intervals", true);
    parser.addArgument(warmUpIntervals);

    description = "Indicates the format to use for timestamps included in " +
         "the output.  A value of 'none' indicates that no timestamps should " +
         "be included.  A value of 'with-date' indicates that both the date " +
         "and the time should be included.  A value of 'without-date' " +
         "indicates that only the time should be included.";
    final Set<String> allowedFormats =
         StaticUtils.setOf("none", "with-date", "without-date");
    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
         "{format}", description, allowedFormats, "none");
    timestampFormat.addLongIdentifier("timestamp-format", true);
    parser.addArgument(timestampFormat);

    description = "Indicates that the client should operate in asynchronous " +
         "mode, in which it will not be necessary to wait for a response to " +
         "a previous request before sending the next request.  Either the " +
         "'--ratePerSecond' or the '--maxOutstandingRequests' argument must " +
         "be provided to limit the number of outstanding requests.";
    asynchronousMode = new BooleanArgument('a', "asynchronous", description);
    parser.addArgument(asynchronousMode);

    description = "Specifies the maximum number of outstanding requests " +
         "that should be allowed when operating in asynchronous mode.";
    maxOutstandingRequests = new IntegerArgument('O', "maxOutstandingRequests",
         false, 1, "{num}", description, 1, Integer.MAX_VALUE, (Integer) null);
    maxOutstandingRequests.addLongIdentifier("max-outstanding-requests", true);
    parser.addArgument(maxOutstandingRequests);

    description = "Indicates that information about the result codes for " +
         "failed operations should not be displayed.";
    suppressErrors = new BooleanArgument(null,
         "suppressErrorResultCodes", 1, description);
    suppressErrors.addLongIdentifier("suppress-error-result-codes", true);
    parser.addArgument(suppressErrors);

    description = "Generate output in CSV format rather than a " +
         "display-friendly format";
    csvFormat = new BooleanArgument('c', "csv", 1, description);
    parser.addArgument(csvFormat);

    description = "Specifies the seed to use for the random number generator.";
    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
         description);
    randomSeed.addLongIdentifier("random-seed", true);
    parser.addArgument(randomSeed);


    parser.addExclusiveArgumentSet(baseDN, ldapURL);
    parser.addExclusiveArgumentSet(scope, ldapURL);
    parser.addExclusiveArgumentSet(filter, ldapURL);
    parser.addExclusiveArgumentSet(attributes, ldapURL);

    parser.addRequiredArgumentSet(filter, ldapURL);

    parser.addDependentArgumentSet(asynchronousMode, ratePerSecond,
         maxOutstandingRequests);
    parser.addDependentArgumentSet(maxOutstandingRequests, asynchronousMode);

    parser.addExclusiveArgumentSet(asynchronousMode, simplePageSize);
  }



  /**
   * Indicates whether this tool supports creating connections to multiple
   * servers.  If it is to support multiple servers, then the "--hostname" and
   * "--port" arguments will be allowed to be provided multiple times, and
   * will be required to be provided the same number of times.  The same type of
   * communication security and bind credentials will be used for all servers.
   *
   * @return  {@code true} if this tool supports creating connections to
   *          multiple servers, or {@code false} if not.
   */
  @Override()
  protected boolean supportsMultipleServers()
  {
    return true;
  }



  /**
   * Retrieves the connection options that should be used for connections
   * created for use with this tool.
   *
   * @return  The connection options that should be used for connections created
   *          for use with this tool.
   */
  @Override()
  public LDAPConnectionOptions getConnectionOptions()
  {
    final LDAPConnectionOptions options = new LDAPConnectionOptions();
    options.setUseSynchronousMode(! asynchronousMode.isPresent());
    return options;
  }



  /**
   * Performs the actual processing for this tool.  In this case, it gets a
   * connection to the directory server and uses it to perform the requested
   * searches.
   *
   * @return  The result code for the processing that was performed.
   */
  @Override()
  public ResultCode doToolProcessing()
  {
    // If the sample rate file argument was specified, then generate the sample
    // variable rate data file and return.
    if (sampleRateFile.isPresent())
    {
      try
      {
        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
        return ResultCode.SUCCESS;
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        err("An error occurred while trying to write sample variable data " +
             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
             "':  ", StaticUtils.getExceptionMessage(e));
        return ResultCode.LOCAL_ERROR;
      }
    }


    // Determine the random seed to use.
    final Long seed;
    if (randomSeed.isPresent())
    {
      seed = Long.valueOf(randomSeed.getValue());
    }
    else
    {
      seed = null;
    }

    // Create value patterns for the base DN, filter, LDAP URL, and proxied
    // authorization DN.
    final ValuePattern dnPattern;
    try
    {
      if (baseDN.getNumOccurrences() > 0)
      {
        dnPattern = new ValuePattern(baseDN.getValue(), seed);
      }
      else if (ldapURL.isPresent())
      {
        dnPattern = null;
      }
      else
      {
        dnPattern = new ValuePattern("", seed);
      }
    }
    catch (final ParseException pe)
    {
      Debug.debugException(pe);
      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
      return ResultCode.PARAM_ERROR;
    }

    final ValuePattern filterPattern;
    try
    {
      if (filter.isPresent())
      {
        filterPattern = new ValuePattern(filter.getValue(), seed);
      }
      else
      {
        filterPattern = null;
      }
    }
    catch (final ParseException pe)
    {
      Debug.debugException(pe);
      err("Unable to parse the filter pattern:  ", pe.getMessage());
      return ResultCode.PARAM_ERROR;
    }

    final ValuePattern ldapURLPattern;
    try
    {
      if (ldapURL.isPresent())
      {
        ldapURLPattern = new ValuePattern(ldapURL.getValue(), seed);
      }
      else
      {
        ldapURLPattern = null;
      }
    }
    catch (final ParseException pe)
    {
      Debug.debugException(pe);
      err("Unable to parse the LDAP URL pattern:  ", pe.getMessage());
      return ResultCode.PARAM_ERROR;
    }

    final ValuePattern authzIDPattern;
    if (proxyAs.isPresent())
    {
      try
      {
        authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
      }
      catch (final ParseException pe)
      {
        Debug.debugException(pe);
        err("Unable to parse the proxied authorization pattern:  ",
            pe.getMessage());
        return ResultCode.PARAM_ERROR;
      }
    }
    else
    {
      authzIDPattern = null;
    }


    // Get the alias dereference policy to use.
    final DereferencePolicy derefPolicy;
    final String derefValue =
         StaticUtils.toLowerCase(dereferencePolicy.getValue());
    if (derefValue.equals("always"))
    {
      derefPolicy = DereferencePolicy.ALWAYS;
    }
    else if (derefValue.equals("search"))
    {
      derefPolicy = DereferencePolicy.SEARCHING;
    }
    else if (derefValue.equals("find"))
    {
      derefPolicy = DereferencePolicy.FINDING;
    }
    else
    {
      derefPolicy = DereferencePolicy.NEVER;
    }


    // Get the set of controls to include in search requests.
    final ArrayList<Control> controlList = new ArrayList<>(5);
    if (assertionFilter.isPresent())
    {
      controlList.add(new AssertionRequestControl(assertionFilter.getValue()));
    }

    if (sortOrder.isPresent())
    {
      final ArrayList<SortKey> sortKeys = new ArrayList<>(5);
      final StringTokenizer tokenizer =
           new StringTokenizer(sortOrder.getValue(), ",");
      while (tokenizer.hasMoreTokens())
      {
        String token = tokenizer.nextToken().trim();

        final boolean ascending;
        if (token.startsWith("+"))
        {
          ascending = true;
          token = token.substring(1);
        }
        else if (token.startsWith("-"))
        {
          ascending = false;
          token = token.substring(1);
        }
        else
        {
          ascending = true;
        }

        final String attributeName;
        final String matchingRuleID;
        final int colonPos = token.indexOf(':');
        if (colonPos < 0)
        {
          attributeName = token;
          matchingRuleID = null;
        }
        else
        {
          attributeName = token.substring(0, colonPos);
          matchingRuleID = token.substring(colonPos+1);
        }

        sortKeys.add(new SortKey(attributeName, matchingRuleID, (! ascending)));
      }

      controlList.add(new ServerSideSortRequestControl(sortKeys));
    }

    if (control.isPresent())
    {
      controlList.addAll(control.getValues());
    }


    // Get the attributes to return.
    final String[] attrs;
    if (attributes.isPresent())
    {
      final List<String> attrList = attributes.getValues();
      attrs = new String[attrList.size()];
      attrList.toArray(attrs);
    }
    else
    {
      attrs = StaticUtils.NO_STRINGS;
    }


    // If the --ratePerSecond option was specified, then limit the rate
    // accordingly.
    FixedRateBarrier fixedRateBarrier = null;
    if (ratePerSecond.isPresent() || variableRateData.isPresent())
    {
      // We might not have a rate per second if --variableRateData is specified.
      // The rate typically doesn't matter except when we have warm-up
      // intervals.  In this case, we'll run at the max rate.
      final int intervalSeconds = collectionInterval.getValue();
      final int ratePerInterval =
           (ratePerSecond.getValue() == null)
           ? Integer.MAX_VALUE
           : ratePerSecond.getValue() * intervalSeconds;
      fixedRateBarrier =
           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
    }


    // If --variableRateData was specified, then initialize a RateAdjustor.
    RateAdjustor rateAdjustor = null;
    if (variableRateData.isPresent())
    {
      try
      {
        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
             ratePerSecond.getValue(), variableRateData.getValue());
      }
      catch (final IOException | IllegalArgumentException e)
      {
        Debug.debugException(e);
        err("Initializing the variable rates failed: " + e.getMessage());
        return ResultCode.PARAM_ERROR;
      }
    }


    // If the --maxOutstandingRequests option was specified, then create the
    // semaphore used to enforce that limit.
    final Semaphore asyncSemaphore;
    if (maxOutstandingRequests.isPresent())
    {
      asyncSemaphore = new Semaphore(maxOutstandingRequests.getValue());
    }
    else
    {
      asyncSemaphore = null;
    }


    // Determine whether to include timestamps in the output and if so what
    // format should be used for them.
    final boolean includeTimestamp;
    final String timeFormat;
    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
    {
      includeTimestamp = true;
      timeFormat       = "dd/MM/yyyy HH:mm:ss";
    }
    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
    {
      includeTimestamp = true;
      timeFormat       = "HH:mm:ss";
    }
    else
    {
      includeTimestamp = false;
      timeFormat       = null;
    }


    // Determine whether any warm-up intervals should be run.
    final long totalIntervals;
    final boolean warmUp;
    int remainingWarmUpIntervals = warmUpIntervals.getValue();
    if (remainingWarmUpIntervals > 0)
    {
      warmUp = true;
      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
    }
    else
    {
      warmUp = true;
      totalIntervals = 0L + numIntervals.getValue();
    }


    // Create the table that will be used to format the output.
    final OutputFormat outputFormat;
    if (csvFormat.isPresent())
    {
      outputFormat = OutputFormat.CSV;
    }
    else
    {
      outputFormat = OutputFormat.COLUMNS;
    }

    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
         timeFormat, outputFormat, " ",
         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
                  "Searches/Sec"),
         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
                  "Avg Dur ms"),
         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
                  "Entries/Srch"),
         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
                  "Errors/Sec"),
         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
                  "Searches/Sec"),
         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
                  "Avg Dur ms"));


    // Create values to use for statistics collection.
    final AtomicLong        searchCounter   = new AtomicLong(0L);
    final AtomicLong        entryCounter    = new AtomicLong(0L);
    final AtomicLong        errorCounter    = new AtomicLong(0L);
    final AtomicLong        searchDurations = new AtomicLong(0L);
    final ResultCodeCounter rcCounter       = new ResultCodeCounter();


    // Determine the length of each interval in milliseconds.
    final long intervalMillis = 1000L * collectionInterval.getValue();


    // Create the threads to use for the searches.
    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
    final SearchRateThread[] threads =
         new SearchRateThread[numThreads.getValue()];
    for (int i=0; i < threads.length; i++)
    {
      final LDAPConnection connection;
      try
      {
        connection = getConnection();
      }
      catch (final LDAPException le)
      {
        Debug.debugException(le);
        err("Unable to connect to the directory server:  ",
            StaticUtils.getExceptionMessage(le));
        return le.getResultCode();
      }

      threads[i] = new SearchRateThread(this, i, connection,
           asynchronousMode.isPresent(), dnPattern, scope.getValue(),
           derefPolicy, sizeLimit.getValue(), timeLimitSeconds.getValue(),
           typesOnly.isPresent(), filterPattern, attrs, ldapURLPattern,
           authzIDPattern, simplePageSize.getValue(), controlList,
           iterationsBeforeReconnect.getValue(), runningThreads, barrier,
           searchCounter, entryCounter, searchDurations, errorCounter,
           rcCounter, fixedRateBarrier, asyncSemaphore);
      threads[i].start();
    }


    // Display the table header.
    for (final String headerLine : formatter.getHeaderLines(true))
    {
      out(headerLine);
    }


    // Start the RateAdjustor before the threads so that the initial value is
    // in place before any load is generated unless we're doing a warm-up in
    // which case, we'll start it after the warm-up is complete.
    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
    {
      rateAdjustor.start();
    }


    // Indicate that the threads can start running.
    try
    {
      barrier.await();
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
    }

    long overallStartTime = System.nanoTime();
    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;


    boolean setOverallStartTime = false;
    long    lastDuration        = 0L;
    long    lastNumEntries      = 0L;
    long    lastNumErrors       = 0L;
    long    lastNumSearches     = 0L;
    long    lastEndTime         = System.nanoTime();
    for (long i=0; i < totalIntervals; i++)
    {
      if (rateAdjustor != null)
      {
        if (! rateAdjustor.isAlive())
        {
          out("All of the rates in " + variableRateData.getValue().getName() +
              " have been completed.");
          break;
        }
      }

      final long startTimeMillis = System.currentTimeMillis();
      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
      nextIntervalStartTime += intervalMillis;
      if (sleepTimeMillis > 0)
      {
        sleeper.sleep(sleepTimeMillis);
      }

      if (stopRequested.get())
      {
        break;
      }

      final long endTime          = System.nanoTime();
      final long intervalDuration = endTime - lastEndTime;

      final long numSearches;
      final long numEntries;
      final long numErrors;
      final long totalDuration;
      if (warmUp && (remainingWarmUpIntervals > 0))
      {
        numSearches   = searchCounter.getAndSet(0L);
        numEntries    = entryCounter.getAndSet(0L);
        numErrors     = errorCounter.getAndSet(0L);
        totalDuration = searchDurations.getAndSet(0L);
      }
      else
      {
        numSearches   = searchCounter.get();
        numEntries    = entryCounter.get();
        numErrors     = errorCounter.get();
        totalDuration = searchDurations.get();
      }

      final long recentNumSearches = numSearches - lastNumSearches;
      final long recentNumEntries = numEntries - lastNumEntries;
      final long recentNumErrors = numErrors - lastNumErrors;
      final long recentDuration = totalDuration - lastDuration;

      final double numSeconds = intervalDuration / 1_000_000_000.0d;
      final double recentSearchRate = recentNumSearches / numSeconds;
      final double recentErrorRate  = recentNumErrors / numSeconds;

      final double recentAvgDuration;
      final double recentEntriesPerSearch;
      if (recentNumSearches > 0L)
      {
        recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches;
        recentAvgDuration =
             1.0d * recentDuration / recentNumSearches / 1_000_000;
      }
      else
      {
        recentEntriesPerSearch = 0.0d;
        recentAvgDuration = 0.0d;
      }


      if (warmUp && (remainingWarmUpIntervals > 0))
      {
        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
             recentEntriesPerSearch, recentErrorRate, "warming up",
             "warming up"));

        remainingWarmUpIntervals--;
        if (remainingWarmUpIntervals == 0)
        {
          out("Warm-up completed.  Beginning overall statistics collection.");
          setOverallStartTime = true;
          if (rateAdjustor != null)
          {
            rateAdjustor.start();
          }
        }
      }
      else
      {
        if (setOverallStartTime)
        {
          overallStartTime    = lastEndTime;
          setOverallStartTime = false;
        }

        final double numOverallSeconds =
             (endTime - overallStartTime) / 1_000_000_000.0d;
        final double overallSearchRate = numSearches / numOverallSeconds;

        final double overallAvgDuration;
        if (numSearches > 0L)
        {
          overallAvgDuration = 1.0d * totalDuration / numSearches / 1_000_000;
        }
        else
        {
          overallAvgDuration = 0.0d;
        }

        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
             recentEntriesPerSearch, recentErrorRate, overallSearchRate,
             overallAvgDuration));

        lastNumSearches = numSearches;
        lastNumEntries  = numEntries;
        lastNumErrors   = numErrors;
        lastDuration    = totalDuration;
      }

      final List<ObjectPair<ResultCode,Long>> rcCounts =
           rcCounter.getCounts(true);
      if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty()))
      {
        err("\tError Results:");
        for (final ObjectPair<ResultCode,Long> p : rcCounts)
        {
          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
        }
      }

      lastEndTime = endTime;
    }


    // Shut down the RateAdjustor if we have one.
    if (rateAdjustor != null)
    {
      rateAdjustor.shutDown();
    }


    // Stop all of the threads.
    ResultCode resultCode = ResultCode.SUCCESS;
    for (final SearchRateThread t : threads)
    {
      t.signalShutdown();
    }
    for (final SearchRateThread t : threads)
    {
      final ResultCode r = t.waitForShutdown();
      if (resultCode == ResultCode.SUCCESS)
      {
        resultCode = r;
      }
    }

    return resultCode;
  }



  /**
   * Requests that this tool stop running.  This method will attempt to wait
   * for all threads to complete before returning control to the caller.
   */
  public void stopRunning()
  {
    stopRequested.set(true);
    sleeper.wakeup();

    while (true)
    {
      final int stillRunning = runningThreads.get();
      if (stillRunning <= 0)
      {
        break;
      }
      else
      {
        try
        {
          Thread.sleep(1L);
        } catch (final Exception e) {}
      }
    }
  }



  /**
   * Retrieves the maximum number of outstanding requests that may be in
   * progress at any time, if appropriate.
   *
   * @return  The maximum number of outstanding requests that may be in progress
   *          at any time, or -1 if the tool was not configured to perform
   *          asynchronous searches with a maximum number of outstanding
   *          requests.
   */
  int getMaxOutstandingRequests()
  {
    if (maxOutstandingRequests.isPresent())
    {
      return maxOutstandingRequests.getValue();
    }
    else
    {
      return -1;
    }
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public LinkedHashMap<String[],String> getExampleUsages()
  {
    final LinkedHashMap<String[],String> examples =
         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));

    String[] args =
    {
      "--hostname", "server.example.com",
      "--port", "389",
      "--bindDN", "uid=admin,dc=example,dc=com",
      "--bindPassword", "password",
      "--baseDN", "dc=example,dc=com",
      "--scope", "sub",
      "--filter", "(uid=user.[1-1000000])",
      "--attribute", "givenName",
      "--attribute", "sn",
      "--attribute", "mail",
      "--numThreads", "10"
    };
    String description =
         "Test search performance by searching randomly across a set " +
         "of one million users located below 'dc=example,dc=com' with ten " +
         "concurrent threads.  The entries returned to the client will " +
         "include the givenName, sn, and mail attributes.";
    examples.put(args, description);

    args = new String[]
    {
      "--generateSampleRateFile", "variable-rate-data.txt"
    };
    description =
         "Generate a sample variable rate definition file that may be used " +
         "in conjunction with the --variableRateData argument.  The sample " +
         "file will include comments that describe the format for data to be " +
         "included in this file.";
    examples.put(args, description);

    return examples;
  }
}
